-
Notifications
You must be signed in to change notification settings - Fork 1
feat(docs): Shadcn Versioning POC #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🚀 Preview deploymentBranch: 📝 Preview URL: https://auth0-universal-components-a1gk2q0cc-okta.vercel.app Updated at 2026-01-08T21:48:56.440Z |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #49 +/- ##
========================================
Coverage 83.17% 83.17%
========================================
Files 125 125
Lines 10291 10291
Branches 1007 1262 +255
========================================
Hits 8560 8560
Misses 1731 1731 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
|
||
| if (fs.existsSync(versionedPath)) { | ||
| try { | ||
| const content = fs.readFileSync(versionedPath, 'utf-8'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Semgrep identified an issue in your code:
User-controlled URL path is used to read files without traversal validation; attackers can escape the intended directory to access arbitrary files like /etc/passwd or .env.
More details about this
The application reads a file from versionedPath without validating that the path stays within the intended public/r/{version} directory. Since fileName is extracted directly from the user-controlled URL (url.pathname.replace(/^\/r\//, '')), an attacker can use path traversal sequences like ../ to escape the directory and access arbitrary files.
Exploit scenario:
- Attacker sends a request:
GET /r/../../etc/passwd?version=v1 - The
fileNamevariable is set to../../etc/passwd path.join(process.cwd(), 'public', 'r', 'v1', '../../etc/passwd')resolves to{cwd}/etc/passwd(traversing up and out of the intended directory)fs.readFileSync(versionedPath)reads the system's/etc/passwdfile, exposing sensitive system information- The attacker receives the file contents in the HTTP response
Similarly, an attacker could target ../../.env to leak environment variables containing database credentials or API keys, or ../../config.json to access application secrets.
Dataflow graph
flowchart LR
classDef invis fill:white, stroke: none
classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none
subgraph File0["<b>docs-site/src/api/registry-middleware.ts</b>"]
direction LR
%% Source
subgraph Source
direction LR
v0["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] req.headers</a>"]
end
%% Intermediate
subgraph Traces0[Traces]
direction TB
v2["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] `</a>"]
v3["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] url</a>"]
v4["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L20 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 20] fileName</a>"]
v5["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L33 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 33] versionedPath</a>"]
end
v2 --> v3
v3 --> v4
v4 --> v5
%% Sink
subgraph Sink
direction LR
v1["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L37 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 37] versionedPath</a>"]
end
end
%% Class Assignment
Source:::invis
Sink:::invis
Traces0:::invis
File0:::invis
%% Connections
Source --> Traces0
Traces0 --> Sink
To resolve this comment:
✨ Commit Assistant fix suggestion
| const content = fs.readFileSync(versionedPath, 'utf-8'); | |
| // Validate and normalize the file path to prevent path traversal attacks | |
| const normalizedFileName = path.normalize(fileName).replace(/^(\.\.[\/\\])+/, ''); | |
| if (normalizedFileName !== fileName || normalizedFileName.includes('..')) { | |
| res.statusCode = 400; | |
| res.setHeader('Content-Type', 'application/json'); | |
| res.end(JSON.stringify({ error: 'Invalid file path' })); | |
| return; | |
| } | |
| const baseDir = path.resolve(process.cwd(), 'public', 'r', version); | |
| const versionedPath = path.join(baseDir, normalizedFileName); | |
| // Verify the resolved path stays within the base directory | |
| const resolvedPath = path.resolve(versionedPath); | |
| if (!resolvedPath.startsWith(baseDir + path.sep) && resolvedPath !== baseDir) { | |
| res.statusCode = 403; | |
| res.setHeader('Content-Type', 'application/json'); | |
| res.end(JSON.stringify({ error: 'Access denied' })); | |
| return; | |
| } | |
| const content = fs.readFileSync(resolvedPath, 'utf-8'); |
View step-by-step instructions
- Import Node.js's
path.normalize()andpath.resolve()functions, which are already available in yourpathdependency. - Add validation to ensure
fileNamedoesn't contain path traversal sequences before constructing the file path. Add this check after extractingfileName:const normalizedFileName = path.normalize(fileName).replace(/^(\.\.[\/\\])+/, ''); if (normalizedFileName !== fileName || normalizedFileName.includes('..')) { res.statusCode = 400; res.end(JSON.stringify({ error: 'Invalid file path' })); return; }
- Use
path.resolve()to get the absolute path of your public directory base:const baseDir = path.resolve(process.cwd(), 'public', 'r', version); - Construct the full file path using the normalized fileName:
const versionedPath = path.join(baseDir, normalizedFileName); - Add a security check to verify the resolved path stays within the base directory:
This prevents attackers from using sequences like
const resolvedPath = path.resolve(versionedPath); if (!resolvedPath.startsWith(baseDir)) { res.statusCode = 403; res.end(JSON.stringify({ error: 'Access denied' })); return; }
../../../etc/passwdto access files outside your registry directory.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by express-fs-filename.
You can view more details about this finding in the Semgrep AppSec Platform.
|
|
||
| const versionedPath = path.join(process.cwd(), 'public', 'r', version, fileName); | ||
|
|
||
| if (fs.existsSync(versionedPath)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Semgrep identified an issue in your code:
User-controlled file path from URL allows path traversal to read arbitrary files outside the intended directory.
More details about this
The versionedPath variable is constructed using the fileName parameter extracted from the user's URL, which comes directly from req.url without validation. An attacker can exploit this by crafting a malicious URL to traverse outside the intended public/r/ directory.
Exploit scenario:
- Attacker sends a request:
/r/../../config.json?version=v1 - The code extracts
fileNameas../../config.jsonfrom the URL pathname path.join(process.cwd(), 'public', 'r', 'v1', '../../config.json')resolves toprocess.cwd()/config.json, escaping the intended directoryfs.existsSync(versionedPath)checks this attacker-controlled path- If the file exists,
fs.readFileSync(versionedPath)reads sensitive application config files that should not be accessible
Even though the SPECIAL_FILES allowlist exists, it only blocks exact filename matches like index.json. Path traversal sequences like ../, ..\\, or encoded variants bypass this check entirely, allowing attackers to read arbitrary files within the filesystem that the Node.js process has permissions to access.
Dataflow graph
flowchart LR
classDef invis fill:white, stroke: none
classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none
subgraph File0["<b>docs-site/src/api/registry-middleware.ts</b>"]
direction LR
%% Source
subgraph Source
direction LR
v0["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] req.headers</a>"]
end
%% Intermediate
subgraph Traces0[Traces]
direction TB
v2["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] `</a>"]
v3["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L19 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 19] url</a>"]
v4["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L20 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 20] fileName</a>"]
v5["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L33 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 33] versionedPath</a>"]
end
v2 --> v3
v3 --> v4
v4 --> v5
%% Sink
subgraph Sink
direction LR
v1["<a href=https://github.com/auth0/auth0-ui-components/blob/245a83a11e15b5b566c3ab4271461cd2448a848c/docs-site/src/api/registry-middleware.ts#L35 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 35] versionedPath</a>"]
end
end
%% Class Assignment
Source:::invis
Sink:::invis
Traces0:::invis
File0:::invis
%% Connections
Source --> Traces0
Traces0 --> Sink
To resolve this comment:
✨ Commit Assistant fix suggestion
| if (fs.existsSync(versionedPath)) { | |
| // Sanitize and validate the file path to prevent directory traversal attacks | |
| const normalizedFileName = path.normalize(fileName).replace(/^(\.\.(\/|\\|$))+/, ''); | |
| const baseDir = path.resolve(process.cwd(), 'public', 'r', version); | |
| const versionedPath = path.resolve(baseDir, normalizedFileName); | |
| // Security check: ensure the resolved path stays within the intended directory | |
| if (!versionedPath.startsWith(baseDir + path.sep) && versionedPath !== baseDir) { | |
| res.statusCode = 400; | |
| res.setHeader('Content-Type', 'application/json'); | |
| res.end(JSON.stringify({ error: 'Invalid file path' })); | |
| return; | |
| } | |
| if (fs.existsSync(versionedPath)) { |
View step-by-step instructions
-
Import Node.js's
path.normalize()andpath.resolve()functions at the top of the file (these are already available from the importedpathmodule). -
After extracting
fileNamefrom the URL, validate and sanitize it by normalizing the path and ensuring it doesn't contain directory traversal sequences:const fileName = url.pathname.replace(/^\/r\//, ''); const normalizedFileName = path.normalize(fileName).replace(/^(\.\.(\/|\\|$))+/, '');
This removes any
../or..\sequences that could allow directory traversal. -
Create a safe base directory path using
path.resolve()to get the absolute path:const baseDir = path.resolve(process.cwd(), 'public', 'r', version);
-
Build the full file path and resolve it to an absolute path:
const versionedPath = path.resolve(baseDir, normalizedFileName);
-
Add a security check to verify the resolved path is still within the intended directory:
if (!versionedPath.startsWith(baseDir)) { res.statusCode = 400; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ error: 'Invalid file path' })); return; }
This ensures that even after path resolution, the file remains within the
public/r/version/directory and prevents access to files outside this directory.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by express-fs-filename.
You can view more details about this finding in the Semgrep AppSec Platform.
feat: implement component versioning with vercel serverless functions revert: remove registry versioning menu item from layout fix: add registry api rewrite to root vercel.json for ci/cd deployments fix: add functions configuration to locate api directory
a25a6b3 to
1c203ba
Compare
Changes
Server-Side Registry Versioning
Implements query parameter-based versioning for shadcn registry components.
Changes
/r/requests and routes based on?version=parameterv1/andv2/folders?version=latestmaps to newest version (v2)versions.jsonfor version informationUsage
References
Please include relevant links supporting this change such as a:
Testing
Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and tests should be added for new functionality and existing tests should complete without errors.
Checklist